Repository navigation
fix(migrate-ts): prove view replace legal on live Postgres before planning it - #429
Merged
Merged
Conversation
Flyway history filter (migrate, verify, gen advice), view replace legality decided by a rolled-back Postgres probe, --allow hints, view recreate scope, report @via dimension nullability in every port, Kotlin <Report>Names, oracle CLI providers, prerelease-link and prerelease:publish fixes, doc-writer timestamps removed.
…tracked table changes
…reads changed tables Postgres: a view is dropped and created, not replaced, when the plan retypes, drops or renames a column of, or drops, a table outside dependsOn that the live body names (X8 exception to X7; a renamed table is matched by its old name). Sibling checks: - rename-table: live Postgres test; no refusal, the proven replace-view applies and the view keeps its OID. No change. - rename-column: covered by the same exception; live Postgres test, drop + create applies. - mysql: migrate-ts Dialect is postgres | sqlite | d1; no MySQL dialect. No change. - sqlite: real bun:sqlite, statements applied one at a time. DROP COLUMN and a table rebuild both fail while a view reading the table stands. Fixed in Pass 2c for every table the stored view body names; executed test added. - d1: same engine, in one transaction with foreign keys on. Native path and the foreign-key cascade both failed; both fixed, including a view the migration drops; executed test added. Not covered: an @Unmanaged live view reading a cascade-rebuilt table is left alone
…ws over retyped untracked tables Pass 2c now covers Postgres: an unchanged view is dropped before and created after a table change when its stored body names an altered table outside dependsOn and the altered column (column-scoped). Unit case and an executed live Postgres case added; both fail without the fix. F2: the CHANGELOG bullet "SQLite and D1 are unchanged" now says the replace rule does not apply to SQLite and D1. MySQL: migrate-ts Dialect is postgres | sqlite | d1; no MySQL path exists, no change
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
migrate-tsnow runs a plan-time live probe for Postgres views. When a live view differs from its expected definition, the probe creates the expected view in a rolled-back transaction and compares column names and types in order. Only a proven-legal change plansCREATE OR REPLACE VIEW; otherwise the plan drops and recreates the view. If the probe cannot run, the plan drops and recreates. The tool's own type prediction no longer decides legality. Views whose live body reads a table that changes are recreated, and the same recreate rule covers SQLite and D1.meta migrate --from-dbwith--migration-format flywayno longer proposes droppingflyway_schema_history. The blocked-view hint now names a--allowtoken the parser accepts, andmeta verifyprints view changes with a reason instead of bare view pairs.object.reportexposes its fields and referenceenforce, Kotlin emits a<Entity>Namesfile for reports, Java and C# report shapes and dimension nullability throughviarequired references are corrected, and the docs "Generated On" timestamp is stable across runs. The prerelease script now pins Maven8.1.0-rc.N, publishes the Maven candidate, and no longer writes stray files in linked worktrees.Notes
bigintwidening ofcount/sum, the legacy@tableview-source name, and the TypeScript plural rename.any/allaggregate is out of scope for this PR.Validation
Risk Assessment
Testing
Live: 103 tests in view-replace-probe-pg.test.ts and view-adopt-typed-pg.test.ts against a containerised Postgres, all pass. Live: 53 tests across six SQLite, D1 and Postgres view lifecycle and drift files, all pass. Not live: migrate-ts unit suite, 578 pass, supporting evidence only. Not live: full server suite (
bun testin server/typescript), 10761 pass, 0 fail, 50 skip, 1041 files, log at the evidence path below. The MySQL scenario is untested because migrate-ts has no MySQL dialect.Evidence: Full server test suite log (10761 pass, 0 fail, not live)
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
server/typescript/packages/migrate-ts/src/diff/index.ts:991- Sibling path of Z1 that stays reachable: the probe proves a replace against the schema before the plan's table changes, and Pass 2c only supersedes that replace for tables listed in the view'sdependsOn. An@sqlview'sdependsOnis partial by design:collectSqlDependsOn(codegen-ts/src/projection/build-projection-views.ts:335) records only the host table, the extends anchors and a report's@fromtable, never a table the opaque body joins. Sequence: a stamped report with an@sqlviewSELECT s.id, o.amount FROM sales s JOIN orders o ...(@from: Sale, sodependsOn = [sales]). The author edits the body and, in the same change, widensOrder.amountfrom int to long.meta migrate --from-db: Pass 2 queueschange-column-type orders.amount;pushViewUpdateprobes, the temporary view and the live view both reportamount integer, so the plan isreplace-viewwithproven; Pass 2c (line 1186) finds no altered table independsOnand leaves it. The emitter runsALTER TABLE orders ALTER COLUMN amount TYPE bigintat stage 2, beforereplace-viewat stage 7, and Postgres refuses it:cannot alter type of a column used by a view or rule. The same holds for adrop-columnon the joined table that the live body reads. This is a regression for a managed@sqlchange: at base an opaque body had no column list, so a managed change was alwaysdrop-view(stage 0) +create-view(stage 7), which applies. An unchanged@sqlview over an untracked join already failed at base (the deferred@dependsOngap, ADR-0043) and adoption was already a replace, so only the managed-change case is new. Generated projection and report bodies carry a completedependsOnand are not affected. Fix: inpushViewUpdate, do not consult the probe (plan drop + create) whenchangesalready holds achange-column-typeordrop-columnon a table outsideexpected.dependsOnwhose column the live or expected body names (viewReadsAnyColumnoveractual.sqlandexpected.sql; the deparsed live body names every column it reads). Pass 2 has run by then. Add a unit case beside 'a body that reads every column, in a plan that adds a column' in view-replace.test.ts, and a live case in view-replace-probe-pg.test.ts where Postgres refuses the ALTER while the view stands.CHANGELOG.md:1074- For the record, not a defect: D6 is closed by documentation and a pinning test, not by a behavior change.fields()on anobject.reportstill returns[]; docs/features/reporting.md:276-283 points toreportReadModel(report, root).fields()instead, and server/typescript/packages/metadata/test/report-shape.test.ts:668 pins both. I checked the second half of D6:enforceis already an accessor onMetaReferenceIdentity(metadata/src/core/identity/meta-identity.ts:100), whichreferenceIdentities()returns, so that half was never a code defect. The intent sanctions a docs-only answer explicitly for G5 only; I read D6's docs answer as a deliberate choice that survived earlier rounds, so I am not raising it as an intent contradiction.🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:
server/typescript/packages/migrate-ts/src/diff/index.ts:991- Sibling path of Z1 that stays reachable: the probe proves a replace against the schema before the plan's table changes, and Pass 2c only supersedes that replace for tables listed in the view'sdependsOn. An@sqlview'sdependsOnis partial by design:collectSqlDependsOn(codegen-ts/src/projection/build-projection-views.ts:335) records only the host table, the extends anchors and a report's@fromtable, never a table the opaque body joins. Sequence: a stamped report with an@sqlviewSELECT s.id, o.amount FROM sales s JOIN orders o ...(@from: Sale, sodependsOn = [sales]). The author edits the body and, in the same change, widensOrder.amountfrom int to long.meta migrate --from-db: Pass 2 queueschange-column-type orders.amount;pushViewUpdateprobes, the temporary view and the live view both reportamount integer, so the plan isreplace-viewwithproven; Pass 2c (line 1186) finds no altered table independsOnand leaves it. The emitter runsALTER TABLE orders ALTER COLUMN amount TYPE bigintat stage 2, beforereplace-viewat stage 7, and Postgres refuses it:cannot alter type of a column used by a view or rule. The same holds for adrop-columnon the joined table that the live body reads. This is a regression for a managed@sqlchange: at base an opaque body had no column list, so a managed change was alwaysdrop-view(stage 0) +create-view(stage 7), which applies. An unchanged@sqlview over an untracked join already failed at base (the deferred@dependsOngap, ADR-0043) and adoption was already a replace, so only the managed-change case is new. Generated projection and report bodies carry a completedependsOnand are not affected. Fix: inpushViewUpdate, do not consult the probe (plan drop + create) whenchangesalready holds achange-column-typeordrop-columnon a table outsideexpected.dependsOnwhose column the live or expected body names (viewReadsAnyColumnoveractual.sqlandexpected.sql; the deparsed live body names every column it reads). Pass 2 has run by then. Add a unit case beside 'a body that reads every column, in a plan that adds a column' in view-replace.test.ts, and a live case in view-replace-probe-pg.test.ts where Postgres refuses the ALTER while the view stands.CHANGELOG.md:1074- For the record, not a defect: D6 is closed by documentation and a pinning test, not by a behavior change.fields()on anobject.reportstill returns[]; docs/features/reporting.md:276-283 points toreportReadModel(report, root).fields()instead, and server/typescript/packages/metadata/test/report-shape.test.ts:668 pins both. I checked the second half of D6:enforceis already an accessor onMetaReferenceIdentity(metadata/src/core/identity/meta-identity.ts:100), whichreferenceIdentities()returns, so that half was never a code defect. The intent sanctions a docs-only answer explicitly for G5 only; I read D6's docs answer as a deliberate choice that survived earlier rounds, so I am not raising it as an intent contradiction.server/typescript/packages/migrate-ts/src/diff/index.ts:1160- Sibling of Z1 (fix round 4) that the B1 fix (this run, commit 2f97209) left reachable.tableChangesPrecludeReplacetreats a body that reads columns it does not name (viewReadsEveryColumn:SELECT *,NATURAL JOIN) as stale only when the plan holds anadd-column. The same body over another managed VIEW goes stale when the plan replaces that view in place with an appended column, and nothing checks for it.Sequence, on
meta migrate --from-db: projection P1 ownsv_base. A stamped@sqlviewv_top, declared after it, isSELECT b.*, 1 AS extra FROM v_base b. One change appends a computed field to P1 and editsv_top's body (2 AS extra).pushViewUpdateforv_base: the probe reports live[id, a], expected[id, a, b2], so the plan is a provenreplace-view.pushViewUpdateforv_top:columnsMayGrowis true, butchangesholds noadd-columnand no trigger kind, so the probe is asked. It runs against the oldv_base, reports[id, a, extra]on both sides, and the plan is a second provenreplace-view. The emitter writes both at stage 7 in plan order.CREATE OR REPLACE VIEW v_basesucceeds.CREATE OR REPLACE VIEW v_topthen expandsb.*to[id, a, b2, extra]and Postgres refuses it:cannot change name of view column "extra" to "b2". This is the apply-time refusal the ruling says is never planned. At base a managed@sqlchange was always drop + create, so this is a regression for that case. It needsv_baseahead ofv_topin the expected views (declaration order); in the other order the replaces apply.I did not run this (review runs no tests). The Postgres behaviour is the one the Z1 test already pins for a table (
view-replace-probe-pg.test.ts, 'a body change beside an added column').The rest of this class is closed, so one fix ends it:
add-columnunder a*body: covered (line 1160)drop-table: covered by B1 (lines 1161-1166)dropViewCascade, because the dependent is not recreatedFix: at line 1160, when
columnsMayGrow, also return true for a queuedreplace-viewwhose view name the expected body names (columnIdentifierRe(c.view.name).test(expected.sql)).changesholds it only when it is emitted first, which is the failing order. Add a unit case beside 'a body that reads every column, in a plan that adds a column' inmigrate-ts/test/unit/view-replace.test.ts, and a live case inview-replace-probe-pg.test.tswhere Postgres refuses the second replace. Add the case to the list at CHANGELOG.md:1006 and docs/features/migrations-and-drift.md:441.🔧 Fix applied.
5 issues (4 warnings, 1 info) still open:
server/typescript/packages/migrate-ts/src/diff/index.ts:991- Sibling path of Z1 that stays reachable: the probe proves a replace against the schema before the plan's table changes, and Pass 2c only supersedes that replace for tables listed in the view'sdependsOn. An@sqlview'sdependsOnis partial by design:collectSqlDependsOn(codegen-ts/src/projection/build-projection-views.ts:335) records only the host table, the extends anchors and a report's@fromtable, never a table the opaque body joins. Sequence: a stamped report with an@sqlviewSELECT s.id, o.amount FROM sales s JOIN orders o ...(@from: Sale, sodependsOn = [sales]). The author edits the body and, in the same change, widensOrder.amountfrom int to long.meta migrate --from-db: Pass 2 queueschange-column-type orders.amount;pushViewUpdateprobes, the temporary view and the live view both reportamount integer, so the plan isreplace-viewwithproven; Pass 2c (line 1186) finds no altered table independsOnand leaves it. The emitter runsALTER TABLE orders ALTER COLUMN amount TYPE bigintat stage 2, beforereplace-viewat stage 7, and Postgres refuses it:cannot alter type of a column used by a view or rule. The same holds for adrop-columnon the joined table that the live body reads. This is a regression for a managed@sqlchange: at base an opaque body had no column list, so a managed change was alwaysdrop-view(stage 0) +create-view(stage 7), which applies. An unchanged@sqlview over an untracked join already failed at base (the deferred@dependsOngap, ADR-0043) and adoption was already a replace, so only the managed-change case is new. Generated projection and report bodies carry a completedependsOnand are not affected. Fix: inpushViewUpdate, do not consult the probe (plan drop + create) whenchangesalready holds achange-column-typeordrop-columnon a table outsideexpected.dependsOnwhose column the live or expected body names (viewReadsAnyColumnoveractual.sqlandexpected.sql; the deparsed live body names every column it reads). Pass 2 has run by then. Add a unit case beside 'a body that reads every column, in a plan that adds a column' in view-replace.test.ts, and a live case in view-replace-probe-pg.test.ts where Postgres refuses the ALTER while the view stands.CHANGELOG.md:1074- For the record, not a defect: D6 is closed by documentation and a pinning test, not by a behavior change.fields()on anobject.reportstill returns[]; docs/features/reporting.md:276-283 points toreportReadModel(report, root).fields()instead, and server/typescript/packages/metadata/test/report-shape.test.ts:668 pins both. I checked the second half of D6:enforceis already an accessor onMetaReferenceIdentity(metadata/src/core/identity/meta-identity.ts:100), whichreferenceIdentities()returns, so that half was never a code defect. The intent sanctions a docs-only answer explicitly for G5 only; I read D6's docs answer as a deliberate choice that survived earlier rounds, so I am not raising it as an intent contradiction.server/typescript/packages/migrate-ts/src/diff/index.ts:1160- Sibling of Z1 (fix round 4) that the B1 fix (this run, commit 2f97209) left reachable.tableChangesPrecludeReplacetreats a body that reads columns it does not name (viewReadsEveryColumn:SELECT *,NATURAL JOIN) as stale only when the plan holds anadd-column. The same body over another managed VIEW goes stale when the plan replaces that view in place with an appended column, and nothing checks for it.Sequence, on
meta migrate --from-db: projection P1 ownsv_base. A stamped@sqlviewv_top, declared after it, isSELECT b.*, 1 AS extra FROM v_base b. One change appends a computed field to P1 and editsv_top's body (2 AS extra).pushViewUpdateforv_base: the probe reports live[id, a], expected[id, a, b2], so the plan is a provenreplace-view.pushViewUpdateforv_top:columnsMayGrowis true, butchangesholds noadd-columnand no trigger kind, so the probe is asked. It runs against the oldv_base, reports[id, a, extra]on both sides, and the plan is a second provenreplace-view. The emitter writes both at stage 7 in plan order.CREATE OR REPLACE VIEW v_basesucceeds.CREATE OR REPLACE VIEW v_topthen expandsb.*to[id, a, b2, extra]and Postgres refuses it:cannot change name of view column "extra" to "b2". This is the apply-time refusal the ruling says is never planned. At base a managed@sqlchange was always drop + create, so this is a regression for that case. It needsv_baseahead ofv_topin the expected views (declaration order); in the other order the replaces apply.I did not run this (review runs no tests). The Postgres behaviour is the one the Z1 test already pins for a table (
view-replace-probe-pg.test.ts, 'a body change beside an added column').The rest of this class is closed, so one fix ends it:
add-columnunder a*body: covered (line 1160)drop-table: covered by B1 (lines 1161-1166)dropViewCascade, because the dependent is not recreatedFix: at line 1160, when
columnsMayGrow, also return true for a queuedreplace-viewwhose view name the expected body names (columnIdentifierRe(c.view.name).test(expected.sql)).changesholds it only when it is emitted first, which is the failing order. Add a unit case beside 'a body that reads every column, in a plan that adds a column' inmigrate-ts/test/unit/view-replace.test.ts, and a live case inview-replace-probe-pg.test.tswhere Postgres refuses the second replace. Add the case to the list at CHANGELOG.md:1006 and docs/features/migrations-and-drift.md:441.server/typescript/packages/migrate-ts/src/diff/index.ts- Fix B1 and C1 with ONE general, conservative rule instead of another per-case guard (the staleness corners keep recurring because the probe sees the schema BEFORE the plan's changes while the emitter runs replace-view AFTER them). RULE: the probe's proof is only trusted when the plan cannot change what the probed body reads. Concretely, a replace-view for a view may be planned from a probe only if NONE of these holds, else plan drop+create: (a) the plan contains any table change that could alter a column of ANY table (add-column, change-column-type/nullable/default, drop-column, rename-column, plus on SQLite-style dialects the rebuild kinds) and the body is an opaque @SQL body or reads columns it does not name (SELECT , NATURAL JOIN, t.), because dependsOn is partial for @SQL bodies; (b) the body reads columns it does not name AND the plan replaces or drops/creates any OTHER view the body could read (view-on-view), or any column-altering change exists. Generated projection and report bodies name every column and carry a complete dependsOn, so for them keep today's behaviour unchanged (probe decides). Keep it in one named helper with a doc comment stating the invariant, and add unit tests beside view-replace.test.ts for: B1 (joined table not in dependsOn gets a type change), C1 (SELECT * over another view that is replaced with an appended column in the same plan), the existing Z1 case, and that a plain unchanged-schema compatible adoption still replaces in place; plus one live Postgres case in view-replace-probe-pg.test.ts for B1 and one for C1 that APPLIES cleanly. Update CHANGELOG/docs sentence on the probe to mention that a body reading columns it does not name is dropped and recreated when the same change alters any table or view.server/typescript/packages/migrate-ts/src/diff/index.ts:1163- Fix round X7 (this run, commit 7686999) moved a defect.planMayChangeWhatBodyReadsreturns false for every derived body and reads only the EXPECTED view. A derived view whose LIVE body reads a table the new body no longer reads is replaced in place while the same plan drops or retypes that table's column, and Postgres refuses the table change at apply.Sequence, on
meta migrate --from-db --allow drop-column: a stamped report has dimensionlabelwith@of: Week.title,@via: Session.week. The author re-points it to@of: Program.title,@via: Session.program(same column name, both varchar(200)) and removes fieldWeek.title. Pass 2 queuesdrop-column weeks.title.pushViewUpdate(line 987): the body is not opaque and has no*, so the probe is asked. The new body compiles, the columns are equal, and the plan is a provenreplace-view. Pass 2c (line 1216) filters by the expected view'sdependsOn= [sessions, programs].weeksis not in it, so the replace is not superseded. The emitter runsALTER TABLE "weeks" DROP COLUMN "title"at stage 2, before the replace at stage 7. The live view still reads the column, and Postgres refuses:cannot drop column title of table weeks because other objects depend on it. A retype of that column fails the same way (cannot alter type of a column used by a view or rule), and so does removing theWeekentity (cannot drop table weeks because other objects depend on it). The live test at integration-tests/test/view-replace-probe-pg.test.ts:538-556 already pins these three refusals for an@sqlbody. I did not run this sequence (review runs no tests).Scope of the regression: at base a managed change to a report view, or to a projection with a computed,
first,any,allorcollectcolumn, was always drop + create, with the drop at stage 0, so it applied. The previous round'stableChangesPrecludeReplace(commit 2f97209) also covered it, because it checkedactual.sqlfor any view. A projection with only passthrough and count/sum columns already planned this replace at base.Other sites that state the same wrong invariant:
Remedy: pass
actualtoplanMayChangeWhatBodyReadsand return true when a queuedchange-column-type,drop-column,rename-columnordrop-tableis on a table outsideexpected.dependsOnthat the live body names (columnIdentifierRe(table).test(actual.sql); the deparsed live body names every table it reads). Add a unit case in migrate-ts/test/unit/view-replace.test.ts and a live case beside view-replace-probe-pg.test.ts:577, and correct the three sentences above.This needs a decision, not an automatic fix: the recorded X7 ruling says derived bodies keep "probe decides", and this remedy makes one exception to it.
🔧 Fix applied.
8 issues (1 error, 5 warnings, 2 infos) still open:
server/typescript/packages/migrate-ts/src/diff/index.ts:991- Sibling path of Z1 that stays reachable: the probe proves a replace against the schema before the plan's table changes, and Pass 2c only supersedes that replace for tables listed in the view'sdependsOn. An@sqlview'sdependsOnis partial by design:collectSqlDependsOn(codegen-ts/src/projection/build-projection-views.ts:335) records only the host table, the extends anchors and a report's@fromtable, never a table the opaque body joins. Sequence: a stamped report with an@sqlviewSELECT s.id, o.amount FROM sales s JOIN orders o ...(@from: Sale, sodependsOn = [sales]). The author edits the body and, in the same change, widensOrder.amountfrom int to long.meta migrate --from-db: Pass 2 queueschange-column-type orders.amount;pushViewUpdateprobes, the temporary view and the live view both reportamount integer, so the plan isreplace-viewwithproven; Pass 2c (line 1186) finds no altered table independsOnand leaves it. The emitter runsALTER TABLE orders ALTER COLUMN amount TYPE bigintat stage 2, beforereplace-viewat stage 7, and Postgres refuses it:cannot alter type of a column used by a view or rule. The same holds for adrop-columnon the joined table that the live body reads. This is a regression for a managed@sqlchange: at base an opaque body had no column list, so a managed change was alwaysdrop-view(stage 0) +create-view(stage 7), which applies. An unchanged@sqlview over an untracked join already failed at base (the deferred@dependsOngap, ADR-0043) and adoption was already a replace, so only the managed-change case is new. Generated projection and report bodies carry a completedependsOnand are not affected. Fix: inpushViewUpdate, do not consult the probe (plan drop + create) whenchangesalready holds achange-column-typeordrop-columnon a table outsideexpected.dependsOnwhose column the live or expected body names (viewReadsAnyColumnoveractual.sqlandexpected.sql; the deparsed live body names every column it reads). Pass 2 has run by then. Add a unit case beside 'a body that reads every column, in a plan that adds a column' in view-replace.test.ts, and a live case in view-replace-probe-pg.test.ts where Postgres refuses the ALTER while the view stands.CHANGELOG.md:1074- For the record, not a defect: D6 is closed by documentation and a pinning test, not by a behavior change.fields()on anobject.reportstill returns[]; docs/features/reporting.md:276-283 points toreportReadModel(report, root).fields()instead, and server/typescript/packages/metadata/test/report-shape.test.ts:668 pins both. I checked the second half of D6:enforceis already an accessor onMetaReferenceIdentity(metadata/src/core/identity/meta-identity.ts:100), whichreferenceIdentities()returns, so that half was never a code defect. The intent sanctions a docs-only answer explicitly for G5 only; I read D6's docs answer as a deliberate choice that survived earlier rounds, so I am not raising it as an intent contradiction.server/typescript/packages/migrate-ts/src/diff/index.ts:1160- Sibling of Z1 (fix round 4) that the B1 fix (this run, commit 2f97209) left reachable.tableChangesPrecludeReplacetreats a body that reads columns it does not name (viewReadsEveryColumn:SELECT *,NATURAL JOIN) as stale only when the plan holds anadd-column. The same body over another managed VIEW goes stale when the plan replaces that view in place with an appended column, and nothing checks for it.Sequence, on
meta migrate --from-db: projection P1 ownsv_base. A stamped@sqlviewv_top, declared after it, isSELECT b.*, 1 AS extra FROM v_base b. One change appends a computed field to P1 and editsv_top's body (2 AS extra).pushViewUpdateforv_base: the probe reports live[id, a], expected[id, a, b2], so the plan is a provenreplace-view.pushViewUpdateforv_top:columnsMayGrowis true, butchangesholds noadd-columnand no trigger kind, so the probe is asked. It runs against the oldv_base, reports[id, a, extra]on both sides, and the plan is a second provenreplace-view. The emitter writes both at stage 7 in plan order.CREATE OR REPLACE VIEW v_basesucceeds.CREATE OR REPLACE VIEW v_topthen expandsb.*to[id, a, b2, extra]and Postgres refuses it:cannot change name of view column "extra" to "b2". This is the apply-time refusal the ruling says is never planned. At base a managed@sqlchange was always drop + create, so this is a regression for that case. It needsv_baseahead ofv_topin the expected views (declaration order); in the other order the replaces apply.I did not run this (review runs no tests). The Postgres behaviour is the one the Z1 test already pins for a table (
view-replace-probe-pg.test.ts, 'a body change beside an added column').The rest of this class is closed, so one fix ends it:
add-columnunder a*body: covered (line 1160)drop-table: covered by B1 (lines 1161-1166)dropViewCascade, because the dependent is not recreatedFix: at line 1160, when
columnsMayGrow, also return true for a queuedreplace-viewwhose view name the expected body names (columnIdentifierRe(c.view.name).test(expected.sql)).changesholds it only when it is emitted first, which is the failing order. Add a unit case beside 'a body that reads every column, in a plan that adds a column' inmigrate-ts/test/unit/view-replace.test.ts, and a live case inview-replace-probe-pg.test.tswhere Postgres refuses the second replace. Add the case to the list at CHANGELOG.md:1006 and docs/features/migrations-and-drift.md:441.server/typescript/packages/migrate-ts/src/diff/index.ts- Fix B1 and C1 with ONE general, conservative rule instead of another per-case guard (the staleness corners keep recurring because the probe sees the schema BEFORE the plan's changes while the emitter runs replace-view AFTER them). RULE: the probe's proof is only trusted when the plan cannot change what the probed body reads. Concretely, a replace-view for a view may be planned from a probe only if NONE of these holds, else plan drop+create: (a) the plan contains any table change that could alter a column of ANY table (add-column, change-column-type/nullable/default, drop-column, rename-column, plus on SQLite-style dialects the rebuild kinds) and the body is an opaque @SQL body or reads columns it does not name (SELECT , NATURAL JOIN, t.), because dependsOn is partial for @SQL bodies; (b) the body reads columns it does not name AND the plan replaces or drops/creates any OTHER view the body could read (view-on-view), or any column-altering change exists. Generated projection and report bodies name every column and carry a complete dependsOn, so for them keep today's behaviour unchanged (probe decides). Keep it in one named helper with a doc comment stating the invariant, and add unit tests beside view-replace.test.ts for: B1 (joined table not in dependsOn gets a type change), C1 (SELECT * over another view that is replaced with an appended column in the same plan), the existing Z1 case, and that a plain unchanged-schema compatible adoption still replaces in place; plus one live Postgres case in view-replace-probe-pg.test.ts for B1 and one for C1 that APPLIES cleanly. Update CHANGELOG/docs sentence on the probe to mention that a body reading columns it does not name is dropped and recreated when the same change alters any table or view.server/typescript/packages/migrate-ts/src/diff/index.ts:1163- Fix round X7 (this run, commit 7686999) moved a defect.planMayChangeWhatBodyReadsreturns false for every derived body and reads only the EXPECTED view. A derived view whose LIVE body reads a table the new body no longer reads is replaced in place while the same plan drops or retypes that table's column, and Postgres refuses the table change at apply.Sequence, on
meta migrate --from-db --allow drop-column: a stamped report has dimensionlabelwith@of: Week.title,@via: Session.week. The author re-points it to@of: Program.title,@via: Session.program(same column name, both varchar(200)) and removes fieldWeek.title. Pass 2 queuesdrop-column weeks.title.pushViewUpdate(line 987): the body is not opaque and has no*, so the probe is asked. The new body compiles, the columns are equal, and the plan is a provenreplace-view. Pass 2c (line 1216) filters by the expected view'sdependsOn= [sessions, programs].weeksis not in it, so the replace is not superseded. The emitter runsALTER TABLE "weeks" DROP COLUMN "title"at stage 2, before the replace at stage 7. The live view still reads the column, and Postgres refuses:cannot drop column title of table weeks because other objects depend on it. A retype of that column fails the same way (cannot alter type of a column used by a view or rule), and so does removing theWeekentity (cannot drop table weeks because other objects depend on it). The live test at integration-tests/test/view-replace-probe-pg.test.ts:538-556 already pins these three refusals for an@sqlbody. I did not run this sequence (review runs no tests).Scope of the regression: at base a managed change to a report view, or to a projection with a computed,
first,any,allorcollectcolumn, was always drop + create, with the drop at stage 0, so it applied. The previous round'stableChangesPrecludeReplace(commit 2f97209) also covered it, because it checkedactual.sqlfor any view. A projection with only passthrough and count/sum columns already planned this replace at base.Other sites that state the same wrong invariant:
Remedy: pass
actualtoplanMayChangeWhatBodyReadsand return true when a queuedchange-column-type,drop-column,rename-columnordrop-tableis on a table outsideexpected.dependsOnthat the live body names (columnIdentifierRe(table).test(actual.sql); the deparsed live body names every table it reads). Add a unit case in migrate-ts/test/unit/view-replace.test.ts and a live case beside view-replace-probe-pg.test.ts:577, and correct the three sentences above.This needs a decision, not an automatic fix: the recorded X7 ruling says derived bodies keep "probe decides", and this remedy makes one exception to it.
server/typescript/packages/migrate-ts/src/diff/index.ts- Maintainer decision on E1: FIX as the reviewer's remedy. The X7 ruling ('probe decides' for derived bodies) stands, with this ONE stated exception, because the probe only proves the NEW body; it cannot see that the LIVE body still reads a table the same plan alters. Passactual(the live view descriptors, with their live body text/dependencies) to planMayChangeWhatBodyReads and return true (so the plan is drop+create, the probe is not trusted) when a queued change-column-type, drop-column, rename-column or drop-table targets a table that is OUTSIDE expected.dependsOn but that the LIVE body names (use the live body's own table references: pg view body from the introspected view, or pg_depend/pg_rewrite dependency info captured at introspection if that is more exact than a textual match; a textual over-report is acceptable, an under-report is not). ADD: a unit case in view-replace.test.ts and a LIVE Postgres case in view-replace-probe-pg.test.ts covering all three refusals on a DERIVED view (drop column, retype, drop table) EXECUTED against Postgres (apply the emitted SQL, assert it applies cleanly), not only planned. CORRECT the three wrong sentences: the planMayChangeWhatBodyReads doc comment in diff/index.ts (it says a derived body lists every table it reads and the probe decides; add the live-body exception), the CHANGELOG sentence, and docs/features/migrations-and-drift.md. SIBLING CHECK, complete in THIS round: (1) rename-table: confirm with a LIVE Postgres test that a plan renaming a table the LIVE view body reads (outside dependsOn) plus a proven replace-view applies cleanly (Postgres views follow the table by OID, so I expect no refusal; if a refusal exists, fix it the same way), and that rename-column on such a table is handled by the same exception (rename-column is in the exception list above, though Postgres allows it, over-recreating is acceptable). (2) MySQL: migrate-ts has no mysql Dialect (postgres|sqlite|d1 only): say so in your summary, and make no change. (3) SQLite/D1: there is no probe there; replace-view already emits DROP VIEW + CREATE VIEW (emit/sqlite.ts) so no refusal-by-replace exists; but check with a REAL SQLite database (bun:sqlite) whether a table rebuild or DROP COLUMN on a table the LIVE view reads (outside dependsOn) fails at apply, and if it does, extend the same live-body check to the sqlite/d1 recreate path (recreateViewsDependingOnChangedTables / d1-cascade) so the view is dropped before the table change and recreated after; add the executed test. State in your final summary exactly what you checked and the result for each of rename-table, mysql, sqlite, d1. Make the round complete: this is the last validation run.server/typescript/packages/migrate-ts/src/diff/index.ts:1244- Sibling left by fix round X8 (commit 9964716). X8 made SQLite and D1 drop and recreate a view for every table its LIVE body names, including an unchanged@sqlbody that joins a table outsidedependsOn. The same case on Postgres is still not handled, and the comment at lines 1240-1243 says it is ("On Postgres the same case is settled when the view is planned (pushViewUpdate)").pushViewUpdateruns only for a view whose definition changed or is unstamped. A stamped view with an equal fingerprint is skipped at line 926, and the live-body check at line 1244 is guarded by!columnScoped, which is false on Postgres.Sequence, on
meta migrate --from-db(or offline): a stamped@sqlviewSELECT g.id, (SELECT count(*) FROM scores s WHERE s.points > 0) AS scored FROM games g, withdependsOn= [games] (ADR-0043: a joined table is not tracked). The author widensScore.pointsfrom int to long and does not touch the view. Pass 2 queueschange-column-type scores.points. Pass 2b plans nothing for the view. Pass 2c:tracked.filtergives [], the live-body loop is skipped, so no view change. At apply,ALTER TABLE "scores" ALTER COLUMN "points" TYPE BIGINTis refused:cannot alter type of a column used by a view or rule(the refusal view-replace-probe-pg.test.ts:538-556 already pins for a standing view). I traced the plan with an ad-hocdiff()call: Postgres gives[change-column-type], SQLite gives[change-column-type, drop-view, create-view].This is not a regression: base planned the same on Postgres, and ADR-0043 records the untracked join as a known limit. It is reported because X8 closed this exact case on the other two dialects and the comment now states it is closed on Postgres. Only a retype reaches it (a drop needs a body change, which is handled; Postgres allows a rename under a view). A derived view is not affected: an unchanged derived body has a complete
dependsOn.The remedy, not the defect, needs a decision, because it extends the change beyond D2/D4 and the X8 ruling. Option 1: in Pass 2c on Postgres, for a view with
priorpresent, also add each altered table outsidetrackedthatliveViewReadsTable(prior, liveName(t))names, kept column-scoped (recreate only whenviewReadsAnyColumn(prior.sql, cols, ...)), with a unit case in diff-view-recreate-columns.test.ts and a live case in view-replace-probe-pg.test.ts that applies. Option 2: leave the behaviour, correct the comment at lines 1240-1243 to say Postgres covers only a view whose definition also changes, and state the limit in docs/features/migrations-and-drift.md beside the SQLite/D1 paragraph.CHANGELOG.md:1042- Fix round X8 added the bullet at line 1018 ("SQLite and D1 drop a view before changing any table its stored definition reads ... It now is for every table the definition in the database names as well") and left the older bullet at line 1042 in the same entry: "SQLite and D1 are unchanged." The two contradict each other. The older bullet is about the replace rule only. Fix, docs only: reword line 1042 to say the replace rule does not apply to SQLite and D1 (neither hasCREATE OR REPLACE VIEW; a replaced view is written asDROP VIEWandCREATE VIEWthere, as before). docs/features/migrations-and-drift.md already words this correctly and needs no change.🔧 Fix applied.
1 info still open:
CHANGELOG.md:1074- For the record, not a defect: D6 is closed by documentation and a pinning test, not by a behavior change.fields()on anobject.reportstill returns[]; docs/features/reporting.md:276-283 points toreportReadModel(report, root).fields()instead, and server/typescript/packages/metadata/test/report-shape.test.ts:668 pins both. I checked the second half of D6:enforceis already an accessor onMetaReferenceIdentity(metadata/src/core/identity/meta-identity.ts:100), whichreferenceIdentities()returns, so that half was never a code defect. The intent sanctions a docs-only answer explicitly for G5 only; I read D6's docs answer as a deliberate choice that survived earlier rounds, so I am not raising it as an intent contradiction.✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsbun test test/view-replace-probe-pg.test.ts test/view-adopt-typed-pg.test.ts (integration-tests) — 103 pass, 0 fail, live Postgres containerbun test test/view-recreate-drift-sqlite.test.ts test/view-lifecycle-sqlite.test.ts test/d1-243-view-over-rebuilt-table.test.ts test/view-lifecycle-pg.test.ts test/view-recreate-drift-pg.test.ts test/report-views-pg.test.ts (integration-tests) — 53 pass, 0 failbun test test/unit (migrate-ts) — 578 pass, 0 fail (not live)bun test (server/typescript, full suite) — 10761 pass, 0 fail, 50 skip, 1041 files, log ~/.no-mistakes/evidence/01M4NZ4P9PWQPKZF800CGBQHGP/full-server-suite.log (not live)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.